/*
 * Copyright 2007 Rob Nielsen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.proxymatic.apps.autoboundary;

import com.googlecode.proxymatic.core.SimpleObject;

import java.lang.reflect.Method;

public class AutoboundaryLookup extends SimpleObject {
    private static final String DEFAULT_AUTOBOUNDARY_PACKAGE_PREFIX = "autoboundary";
    private static final String DEFAULT_AUTOFACTORY_CLASS_SUFFIX = "Factory";
    private static final String DEFAULT_AUTOFACTORY_CONSTRUCTOR_METHOD_PREFIX = "new";
    private final String autoboundaryPackagePrefix;
    private final String autofactoryClassSuffix;

    private final String autofactoryConstructorMethodPrefix;

    public AutoboundaryLookup() {
        this(DEFAULT_AUTOBOUNDARY_PACKAGE_PREFIX, DEFAULT_AUTOFACTORY_CLASS_SUFFIX,
                DEFAULT_AUTOFACTORY_CONSTRUCTOR_METHOD_PREFIX);
    }

    public AutoboundaryLookup(String autoboundaryPrefix, String autofactoryClassSuffix,
            String autofactoryConstructorMethodPrefix) {
        this.autoboundaryPackagePrefix = autoboundaryPrefix + ".";
        this.autofactoryClassSuffix = autofactoryClassSuffix;
        this.autofactoryConstructorMethodPrefix = autofactoryConstructorMethodPrefix;
    }

    protected String getParameterString() {
        return "package:\"" + autoboundaryPackagePrefix + "\", " +
                "factoryClassSuffix:\"" + autofactoryClassSuffix + "\", " +
                "factoryConstructorPrefix:\"" + autofactoryConstructorMethodPrefix + "\"";
    }

    public boolean isAutoboundaryClass(Class classToCheck) {
        return unwrapArrays(classToCheck).getName().startsWith(autoboundaryPackagePrefix);
    }

    public boolean isAutoboundaryFactoryClass(Class classToCheck) {
        classToCheck = unwrapArrays(classToCheck);
        return isAutoboundaryClass(classToCheck) && classToCheck.getName().endsWith(autofactoryClassSuffix);
    }

    public boolean isAutoboundaryFactoryCreateMethod(Method method) {
        Class<?> factoryClass = method.getDeclaringClass();
        if (!isAutoboundaryFactoryClass(factoryClass)) {
            return false;
        }
        Class realClass = getRealClassForFactory(factoryClass);
        Class<?> returnType = method.getReturnType();

        // FIX ROB Should we allow autoboundary factories to return real objects (as the first clause does)?
        // This makes them real factories.  This may be useful for removing checked exceptions from constructors.
        return returnType.isAssignableFrom(realClass) || isAutoboundaryAssignablePair(returnType, realClass);
    }

    public String getRealClassName(Class autoboundaryClass) {
        if (!isAutoboundaryClass(autoboundaryClass)) {
            throw new IllegalArgumentException(autoboundaryClass + " is not in the package " + autoboundaryPackagePrefix);
        }
        String name = autoboundaryClass.getName();
        if (autoboundaryClass.isArray()) {
            int classNameStart = getClassNameStart(name);
            return name.substring(0, classNameStart) + name.substring(classNameStart + autoboundaryPackagePrefix.length());
        } else {
            return name.substring(autoboundaryPackagePrefix.length());
        }
    }

    public String getRealClassNameForFactory(Class autoboundaryClass) {
        if (!isAutoboundaryFactoryClass(autoboundaryClass)) {
            throw new IllegalArgumentException(autoboundaryClass + " is not a Factory in the package " + autoboundaryPackagePrefix);
        }
        String name = autoboundaryClass.getName();
        if (autoboundaryClass.isArray()) {
            int classNameStart = getClassNameStart(name);
            return name.substring(0, classNameStart) +
                    name.substring(classNameStart + autoboundaryPackagePrefix.length(),
                            name.length() - autofactoryClassSuffix.length() - 1) + ";";
        }
        return name.substring(autoboundaryPackagePrefix.length(), name.length() - autofactoryClassSuffix.length());
    }

    /**
     * Does the autoboundary class represent a superclass or interface of the given real class? An example of an
     * autoboundaryAssignablePair would be autoboundary.java.util.List and java.util.ArrayList.
     *
     * @param autoboundaryClass
     * @param realClass
     * @return
     */
    public boolean isAutoboundaryAssignablePair(Class autoboundaryClass, Class realClass) {
        if (!isAutoboundaryClass(autoboundaryClass)) {
            return false;
        }
        Class convertedAutoboundaryClass = getRealClass(autoboundaryClass);
        return convertedAutoboundaryClass.isAssignableFrom(realClass);
    }

    public boolean isAutoboundaryPair(Class autoboundaryClass, Class realClass) {
        return isAutoboundaryPair(autoboundaryClass, realClass, false);
    }

    public boolean isAutoboundaryPair(Class autoboundaryClass, Class realClass, boolean checkForFactories) {
        return
                (isAutoboundaryClass(autoboundaryClass) && getRealClassName(autoboundaryClass).equals(realClass.getName())) ||
                        (checkForFactories && isAutoboundaryFactoryClass(autoboundaryClass) && getRealClassNameForFactory(autoboundaryClass).equals(realClass.getName()));
    }

    public Class getRealClass(Class autoboundaryClass) {
        try {
            return getRealClassForInstance(autoboundaryClass);
        } catch (IllegalArgumentException e) {
            return getRealClassForFactory(autoboundaryClass);
        }
    }

    public Class getRealClassForInstance(Class autoboundaryClass) {
        String realClassName = getRealClassName(autoboundaryClass);
        try {
            return Class.forName(realClassName);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Real class " + realClassName + " for autoboundary " + autoboundaryClass + " cannot be found.");
        }
    }

    public Class getRealClassForFactory(Class autoboundaryClass) {
        String realClassName = getRealClassNameForFactory(autoboundaryClass);
        try {
            return Class.forName(realClassName);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Real class " + realClassName + " for autoboundary factory " + autoboundaryClass + " cannot be found.");
        }
    }

    private int getClassNameStart(String className) {
        return className.indexOf("[L") + 2;
    }

    private Class unwrapArrays(Class classToCheck) {
        if (classToCheck.isArray()) {
            return unwrapArrays(classToCheck.getComponentType());
        }
        return classToCheck;
    }
}
